// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.map;

import bats.Assets;
import bats.SpatialProperties;
import bats.scene.SceneControl;
import bats.scene.SceneNode;
import com.jme3.bullet.collision.shapes.PlaneCollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.material.Material;
import com.jme3.math.Plane;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import java.io.PrintStream;
import java.util.logging.Logger;
import jme3utilities.xml.XmlOutput;

/**
 * A scene control to manage the ground plane in a world map.
 * <p>
 * Each instance is enabled at creation.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class GroundControl
        extends SceneControl {
    // *************************************************************************
    // constants

    /**
     * maximum radius for receiving shadows (in meters, &gt;0)
     */
    final private static float maxRadiusForShadows = 100f;
    /**
     * thickness of the ground plane (in meters, &gt;0)
     */
    final private static float thickness = 0.01f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(GroundControl.class.getName());
    /**
     * name for the geometry
     */
    final private static String geometryName = "ground";
    /**
     * tag for ground planes in XML files
     */
    final public static String xmlTag = "ground";
    // *************************************************************************
    // constructors

    /**
     * Instantiate an enabled control for a specified scene and elevation.
     *
     * @param scene which scene will contain the ground plane (not null)
     * @param topY y-coordinate for the plane
     */
    public GroundControl(SceneNode scene, float topY) {
        super(scene);
        load(topY);
        assert isEnabled();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Reload the ground plane from assets.
     */
    public void reload() {
        float topY = SpatialProperties.getTopY(spatial);
        delete();
        load(topY);
    }

    /**
     * Serialize the ground plane to an XML stream.
     *
     * @param stream output stream (not null)
     * @param indent
     */
    public void writeXml(PrintStream stream, String indent) {
        assert indent != null;

        stream.printf("%s<%s", indent, xmlTag);

        float topY = SpatialProperties.getTopY(spatial);
        topY *= scene.getScaleFactor(); // convert world units to meters
        XmlOutput.putAttribute(stream, "topY", topY, 4);

        stream.println("/>");
    }
    // *************************************************************************
    // private methods

    /**
     * Create the spatial from assets.
     *
     * @param topY y-coordinate for the plane
     */
    private void load(float topY) {
        /*
         * Create a mesh geometry.
         */
        MapNode map = scene.getWorld().getMap();
        float scaleFactor = scene.getScaleFactor();
        float radius = map.getRadius() / scaleFactor; // world units
        float halfThickness = thickness / (2f * scaleFactor); // world units
        Box slab = new Box(radius, halfThickness, radius);
        Geometry geometry = new Geometry(geometryName, slab);
        geometry.addControl(this);
        /*
         * Apply a material to the geometry.
         */
        Material material = Assets.loadBlockMaterial("dirt");
        spatial.setMaterial(material);
        /*
         * The collision shape is a plane instead of a box or mesh because
         * the former (a) is more effient and (b) has infinite extent.
         *
         * The collision plane coincides with the top face of the mesh.
         */
        Vector3f normal = Vector3f.UNIT_Y; // horizontal
        Plane plane = new Plane(normal, halfThickness);
        PlaneCollisionShape shape = new PlaneCollisionShape(plane);
        /*
         * Add the ground plane to the map.
         */
        map.attach(spatial);
        /*
         * Add a rigid body physics control.
         */
        float mass = 0f; // immobile
        RigidBodyControl rigidBodyControl = new RigidBodyControl(shape, mass);
        spatial.addControl(rigidBodyControl);
        rigidBodyControl.setFriction(MapNode.friction);
        scene.getWorld().getPhysics().addObject(rigidBodyControl);
        /*
         * Set the vertical location of both the geometry and the physics.
         */
        float physicsY = topY - halfThickness;
        Vector3f center = new Vector3f(0f, physicsY, 0f);
        rigidBodyControl.setPhysicsLocation(center);
        /*
         * Set the other properties of the geometry.
         */
        ShadowMode shadowMode;
        if (radius <= maxRadiusForShadows) {
            shadowMode = RenderQueue.ShadowMode.Receive;
        } else {
            shadowMode = RenderQueue.ShadowMode.Off;
        }
        spatial.setShadowMode(shadowMode);
        SpatialProperties.setHardness(spatial, "dirt");
        SpatialProperties.setTopY(spatial, topY);
    }
}